package com.bcs.compiler;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;

import org.mozilla.javascript.FunctionNode;
import org.mozilla.javascript.Node;
import org.mozilla.javascript.Parser;
import org.mozilla.javascript.ScriptOrFnNode;
import org.mozilla.javascript.Token;
import org.mozilla.javascript.Node.Jump;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.bcs.moon.MoonException;
import com.bcs.moon.Scope;
import com.bcs.moon.UnimplementedException;
import com.bcs.moon.XCompile;
import com.sun.org.apache.bcel.internal.classfile.ConstantString;

public class TargetSourceC extends XCompile implements Target {	
	protected PrintWriter out;
	protected StringWriter formDef = new StringWriter();
	protected PrintWriter formWriter = new PrintWriter(formDef);

	public TargetSourceC() {
		super();
		target = this;
	}
	
	public void initializeTarget() {
		fileContent = new StringWriter();
		writer = new PrintWriter(fileContent);
		
		bindString = new StringWriter();
		PrintWriter bindWriter = new PrintWriter(bindString);

		out = writer;
		binder = bindWriter;

		formDef = new StringWriter();
		formWriter = new PrintWriter(formDef);
	}
	
	public void callSimpleFunction(int slot, String fnname) {
		out.println("slot[" + slot + "] = " + fnname + "(cx);");
	}

	public void callVoidRuntime(String fnname, Object param, Object param2) {
		out.println(fnname + "(cx, " + valueToString(param) + ", " + valueToString(param2) + ");");
	}

	public void callBinOp(Object target, String fnname, Object left, Object right) {
		out.println(valueToString(target) + " = " + fnname + "(cx, " + valueToString(left) + ", " + valueToString(right) + ");");
	}
	
	public void callUnaryOp(Object target, String fnname, Object v) {
		out.println(valueToString(target) + " = " + fnname + "(cx, " + valueToString(v) + ");");
	}

	public void assign(Object target, Object v) {
		out.println(valueToString(target) + " = " + valueToString(v) + ";");
	}

	public void assignLocal(int slot, Object v) {
		out.println("slot[" + slot + "] = " + valueToString(v) + ";");
	}

	public void invokeGet(Object target, Object obj, String field) {
		out.println(valueToString(target) +  " = jsget(cx, " + valueToString(obj) + ", " + XCompile.hash(field) + ", \"" + field + "\");");
	}

	public void invokeGetAttr(Object target, Object obj, String field) {
		out.println(valueToString(target) +  " = jsgetattr(cx, " + valueToString(obj) + ", " + XCompile.hash(field) + ", \"" + field + "\");");
	}

	public void invokeGetDesc(Object target, Object obj, String field) {
		out.println(valueToString(target) +  " = jsgetdesc(cx, " + valueToString(obj) + ", " + XCompile.hash(field) + ", \"" + field + "\");");
	}

	public void invokeSet(Object obj, String field, Object v) {
		out.println("jsset(cx, " + valueToString(obj) + ", " + XCompile.hash(field) + ", \"" + field + "\", " + valueToString(v) + ");");
	}

	public void invokeGetElem(Object target, Object obj, Object index) {
		out.println(valueToString(target) +  " = jsgetelem(cx, " + valueToString(obj) + "," + valueToString(index) + ");");
	}

	public void invokeSetElem(Object obj, Object index, Object v) {
		out.println("jssetelem(cx, " + valueToString(obj) + "," + valueToString(index) + "," + valueToString(v) + ");");
	}

	public void invokeNew(Object target, Object classParam, ArrayList<Object> params) {
		for (int idx = 0; idx < params.size(); idx++) {	
			out.println("callFrame[" + idx + "] = " + valueToString(params.get(idx)) + ";");
		}
		out.println(valueToString(target) + " = jsnew(cx, " + valueToString(classParam) + ", callFrame, " + params.size() + ");");
	}
	
	public void invokeInvoke(Object target, Object fn, Object thisValue, ArrayList<Object> params) {
		for (int idx = 0; idx < params.size(); idx++) {	
			out.println("callFrame[" + idx + "] = " + valueToString(params.get(idx)) + ";");
		}
		out.println(valueToString(target) + " = jsinvoke(cx, " + valueToString(fn) + ", " + valueToString(thisValue) + ", callFrame, " + params.size() + ");");
	}
	
	public void assignClosure(Object target, String name) {
		out.println(valueToString(target) +  "= jsclosure(cx, code_" + name + ", frame);");
	}

	private String valueToString(Object v) {
		if (v instanceof ValueThis)
			return "me";
		if (v instanceof ValueNullConstant)
			return "JSVAL_NULL";
		if (v instanceof ValueUndefinedConstant)
			return "JSVAL_VOID";
		if (v instanceof ValueBooleanConstant) {
			if (((ValueBooleanConstant) v).value)
				return "JSVAL_TRUE";
			else
				return "JSVAL_FALSE";
		}
		if (v instanceof ValueGlobal) {
			return "cx->" + ((ValueGlobal) v).name;
		}
		if (v instanceof ValueGlobalSlot) {
			return "cx->slots[" + ((ValueGlobalSlot) v).slot + "]";
		}
		if (v instanceof ValueLocal) {
			return "slot[" + ((ValueLocal) v).slot + "]";
		}
		if (v instanceof ValueNumberConstant) {
			double dv = ((ValueNumberConstant) v).value;
			return constantDouble(dv);
		}
		if (v instanceof ValueStringConstant) {
			String str = ((ValueStringConstant) v).value;
			return constantString(str);
		}
		if (v instanceof ValueScopeLocal) {
			ValueScopeLocal vsl = (ValueScopeLocal) v;
			String result = "frame->";
			for (int idx = 0; idx < vsl.depth; idx++)
				result += "outer->";
			result += "slots[" + vsl.slot + "]";
			return result;
		}
		throw new UnimplementedException();
	}

	public void compileFunction(String name, ScriptOrFnNode node, boolean scriptLevel, Scope priorScope) {
		reset(priorScope);
		
		int subCount = node.getFunctionCount();

		internalFunctionNames= new ArrayList<String>();

		out.println("static jsvalue " + name + "(jscontext *cx, jsframe *outer, jsvalue me, jsvalue *args, int argc) {");

		int paramCount = 0;
		if (node instanceof FunctionNode) {
			FunctionNode fn = (FunctionNode) node;
			paramCount = fn.getParamCount();
			fn.flattenSymbolTable(false);
			String[] names = fn.getParamAndVarNames();
			for (int idx = 0; idx < paramCount; idx++) {
				currentFrame.nextSlot++;
				currentScope.addLocal(names[idx], idx);
			}
		}

		StringWriter localBinderString = new StringWriter(); 
		PrintWriter localBinder = new PrintWriter(localBinderString);
		
		if (scriptLevel) {
			for (int idx = 0; idx < subCount; idx++) {
				FunctionNode fn = node.getFunctionNode(idx);
				String fnname = fn.getFunctionName();
				if (fnname.equals("")) {
					fnname = "function" + nextFunctionId++;
				} else {
					binder.println( valueToString(getGlobalValue(fnname)) + " = bindFunction(cx, code_" + fnname  + ");");
				}
				internalFunctionNames.add(fnname);
			}
		} else {
			for (int idx = 0; idx < subCount; idx++) {
				FunctionNode fn = node.getFunctionNode(idx);
				String fnname = fn.getFunctionName();
				String internalFunctionName = "function" + nextFunctionId++;
				if (!fnname.equals("")) {
					int fnIdx = currentFrame.nextSlot++;
					currentScope.addLocal(fnname, fnIdx);
					localBinder.println(localValue(fnIdx) + " = jsclosure(cx, code_" + internalFunctionName + ",frame);");
				}
				internalFunctionNames.add(internalFunctionName);
			}
		}
		
		localBinder.close();
		
		StringWriter w = new StringWriter(); 
		PrintWriter temp = new PrintWriter(w);
		
		PrintWriter save = out;
		out = temp;
				
		compileCode(node, scriptLevel);
		
		temp.flush();
		
		out = save;
		
		if (enableDebugging)
			out.println("jsframe *frame=enterStackDebug(cx, outer, " + currentFrame.nextSlot + "," + maxArgs + "," + constantString(currentScript) + "," + constantString(getDebugFrame()) + ");");
		else
			out.println("jsframe *frame=enterStack(cx, outer, " + currentFrame.nextSlot + "," + maxArgs + ");");
		out.println("jsvalue *slot = frame->slots;");
		out.println("jsvalue rv;");
		for (int idx = 0; idx < currentFrame.nextSlot; idx++) {
			if (idx < paramCount) {
				out.println("slot[" + idx + "] = argc > " + idx + " ? args[" + idx + "] : JSVAL_VOID;");
			}
		}
		out.println("jsvalue *callFrame = frame->callFrame;");
		out.print(localBinderString.toString());
		out.print(w.toString());
		
//		if (scriptLevel) {
			out.println("leaveFrame(cx, frame);");
			out.println("return JSVAL_VOID;");
			
//		}
		
		out.println("}");
		
		ArrayList<String> functionNames = internalFunctionNames;
		Scope fnScope = null;
		if (!scriptLevel)
			fnScope = currentScope;
		for (int idx = 0; idx < subCount; idx++) {
			String fnname = functionNames.get(idx);
			FunctionNode fn = node.getFunctionNode(idx);
			compileFunction("code_" + fnname, fn, false, fnScope);
			
			constantDecls.add("static jsvalue code_" + fnname + "(jscontext *cx, jsframe *outer, jsvalue me, jsvalue *args, int argc);");
		}
	}

	public void compileCode(Node node, boolean scriptLevel) {
		for (Node n = node.getFirstChild(); n != null; n = n.getNext()) {
			if (n.getLineno() != -1) {
				out.println("// line " + n.getLineno());
				currentLine = n.getLineno();
				
				if (enableDebugging && inUserScript()) {
					int breakpointIdx = breakpointScripts.size();
					breakpointScripts.add(currentScript);
					breakpointLines.add(currentLine);
					out.println("jsDebugCheckBreakpoint(cx," + breakpointIdx + "," + currentLine + ");");
				}
			}
			int t = n.getType();
			if (t == Token.VAR) {
				compileVar(n, scriptLevel);
			} else if (t == Token.EXPR_RESULT || t == Token.EXPR_VOID) {
				Node expr = n.getFirstChild();
				boolean handled= false;
				
				if (expr.getType() == Token.SETNAME) {
					Node child = expr.getFirstChild();
					Integer localSlot = currentScope.getName(child.getString());
	
					if (localSlot != null) {
						compileExpression(child.getNext(), localSlot);
						handled = true;
					} else if (canDirectCompile(child.getNext())) {
						out.println(valueToString(nameToValue(child.getString())) + " = " + valueToString(directValue(child.getNext())) + ";");
						handled = true;
					}
				} 
				if (!handled) {
					int slot = currentFrame.getTempSlot();
					compileExpression(n.getFirstChild(), slot);
					currentFrame.releaseTempSlot(slot);
				}
			} else if (t == Token.FUNCTION) {

			} else if (t == Token.BLOCK) {
				compileCode(n, scriptLevel);
			} else if (t == Token.RETURN) {
				if (n.getFirstChild() != null) {
					int slot = currentFrame.getTempSlot();
					compileExpression(n.getFirstChild(), slot);
					out.println("rv = " + localValue(slot) + ";");
					out.println("leaveFrame(cx, frame);");
					out.println("return rv;");
					currentFrame.releaseTempSlot(slot);
				} else {
					out.println("leaveFrame(cx, frame);");
					out.println("return JSVAL_VOID;");
				}
			} else if (t == Token.IFEQ) {
				int slot = currentFrame.getTempSlot();
				compileExpression(n.getFirstChild(), slot);
				currentFrame.releaseTempSlot(slot);	
				out.println("if (ifeq(" + localValue(slot) +")) goto " + targetLabel( ((Jump) n).target) + ";");
			} else if (t == Token.IFNE) {
				int slot = currentFrame.getTempSlot();
				compileExpression(n.getFirstChild(), slot);
				currentFrame.releaseTempSlot(slot);	
				out.println("if (ifne(" + localValue(slot) +")) goto " + targetLabel( ((Jump) n).target) + ";");
			} else if (t == Token.EMPTY) {
				out.println("//empty");

			} else if (t == Token.LOOP) {
				compileCode(n, scriptLevel);
			} else if (t == Token.GOTO) {
				out.println("goto " + targetLabel(((Jump) n).target) + ";");
			} else if (t == Token.TARGET) {
				out.println(targetLabel(n) + ":;");
			} else {
				System.out.println("Unknown: " + n);
			}
		}
	}

	public void handleCompile(String targetFile, String mainFunction) {
		try {
			for (int idx = 0; idx < scriptNodes.size(); idx++) {
				currentScript = scriptNames.get(idx);
				compileFunction("mainFunction" + idx, scriptNodes.get(idx), true, null);
			}
			for (int idx = 0; idx < functionNodes.size(); idx++) {
				currentScript = functionScripts.get(idx);
				compileFunction("code_" + functionNames.get(idx), functionNodes.get(idx), false, null);
			}

			writer.println("static void formDefinitions(jscontext *cx) {");
			writer.println("jsframe *frame=enterStack(cx, 0, 3, 2);");
			writer.println("jsvalue *slot = frame->slots;");
			writer.println("jsvalue *callFrame = frame->callFrame;");
			writer.println("jsvalue obj, ctl, ctlArr;");
			writer.print(formDef.toString());
			writer.println("leaveFrame(cx, frame);");
			writer.println("}");
			if (enableDebugging) {
				writer.println("static short breakpointLines[] = {");
				for (int idx = 0; idx < breakpointLines.size(); idx++) {
					writer.println(breakpointLines.get(idx) + ",");
				}
				writer.println("};");
				writer.println("static const char *breakpointScripts[] = {");
				for (int idx = 0; idx < breakpointScripts.size(); idx++) {
					writer.println(encodeString(breakpointScripts.get(idx)) + ",");
				}
				writer.println("};");
			}
			
			writer.println("jscontext * " + mainFunction + "() {");
			if (enableDebugging) {
				writer.println("jscontext *cx = jsnewdebugcontext(" + nextGlobalId + "," + breakpointLines.size() + ", breakpointLines, breakpointScripts);");
			} else {
				writer.println("jscontext *cx = jsnewcontext(" + nextGlobalId + ");");
			}
			writer.println("initRuntime(cx);");
			writer.print(bindString.toString());
			for (String c : constantLines)
				writer.println(c);
			for (int idx = 0; idx < scriptNodes.size(); idx++) {
				if (isPriorityFile(scriptNames.get(idx)))
					writer.println("mainFunction" + idx + "(cx, 0, JSVAL_VOID, 0, 0);");
			}
			writer.println("formDefinitions(cx);");
			for (int idx = 0; idx < scriptNodes.size(); idx++) {
				if (!isPriorityFile(scriptNames.get(idx)))
					writer.println("mainFunction" + idx + "(cx, 0, JSVAL_VOID, 0, 0);");
			}
			writer.println("return cx;");
			writer.println("}");
			
			writer.close();
			
			try {
				writer = new PrintWriter(targetFile);
		
				for (String c : constantDecls)
					writer.println(c);
		
				writer.print(fileContent.toString());
				
				writer.close();
			} catch (IOException ex) {
				throw new RuntimeException(ex);
			}
		} catch (MoonException ex) {
			hasError = true;
			errorLine = ex.line;
			errorScript = currentScript;
			errorMessage = ex.description;
		} catch (Exception ex) {
			hasError = true;
			errorLine = -1;
			errorScript = currentScript;
			errorMessage = "An unexpected error occurred: " + ex.toString();	
		}
	}

	public Object parseFunction(String scriptName, String script, String fnname) {
		try {
			currentScript = scriptName;
			Parser parser = new Parser(compilerEnv, errorReporter);
	
			ScriptOrFnNode node = parser.parse(
					"function __int_fn() {" + script + "}", "src://none", 1);
			
			functionNodes.add(node.getFunctionNode(0));
			functionNames.add(fnname);
			functionScripts.add(scriptName);
			
			int globId = nextGlobalId++;
			defineGlobal(fnname, globId);
			binder.println(valueToString(getGlobalValue(fnname)) + " = bindFunction(cx, code_" + fnname  + ");");
			return new ValueGlobalSlot(globId);
		} catch (MoonException ex) {
			hasError = true;
			errorLine = ex.line;
			errorScript = scriptName;
			errorMessage = ex.description;
			return null;
		} catch (Exception ex) {
			hasError = true;
			errorLine = -1;
			errorScript = scriptName;
			errorMessage = "An unexpected error occurred: " + ex.toString();	
			return null;
		}
	}

	protected void compileForm(PrintWriter writer, String scriptName, String obj, Element rootDict, boolean isControl) {
		NodeList children = rootDict.getChildNodes();
		int count = children.getLength();

		int mode = 0;
		String key = null;
		Element controls = null;
		
		String ctlName;
		if (isControl)
			ctlName = getControlName(rootDict);
		else
			ctlName = "_form";
		
		for (int idx = 0; idx < count; idx++) {
			org.w3c.dom.Node n = children.item(idx);
			if (n instanceof Element) {
				Element el = (Element) n;
				if (mode == 0) {
					String nodeName = el.getNodeName();
					if (!nodeName.equals("key"))
						throw new RuntimeException("Malformed property list");
					key = el.getTextContent();
					mode = 1;
				} else {
					String valueType = el.getNodeName();
					if (valueType.equals("false")) {
						writer.println("jsset(cx," + obj + "," + hash(key) + "," + encodeString(key) + ",JSVAL_FALSE);");
					} else if (valueType.equals("true")) {
						writer.println("jsset(cx," + obj + "," + hash(key) + "," + encodeString(key) + ",JSVAL_TRUE);");
					} else if (valueType.equals("integer")) {
						writer.println("jsset(cx," + obj + "," + hash(key) + "," + encodeString(key) + "," + constantDouble(Integer.parseInt(el.getTextContent())) + ");");
					} else if (valueType.equals("string")) {
						if (Character.isUpperCase(key.charAt(0))) {
							String fnscript = el.getTextContent();
							if (fnscript.trim().length() > 0) {
								String fnname = "_eventHandler" + gNextEventHandlerId++;
								Object boundfn = parseFunction(scriptName + "/" + ctlName + "/" + key, fnscript, fnname);
								if (hasError)
									return;
								writer.println("jsset(cx," + obj + "," + hash(key) + "," + encodeString(key) + "," + valueToString(boundfn) + ");");
							}
						} else {
							writer.println("jsset(cx," + obj + "," + hash(key) + "," + encodeString(key) + "," + constantString(el.getTextContent()) + ");");
						}
					} else if (valueType.equals("array") && key.equals("controls")) {
						controls = el;
						writer.println("jsset(cx," + obj + "," + hash(key) + "," + encodeString(key) + ",ctlArr=jsnewarray(cx));");
					}
					mode = 0;
				}
			}
		}
		
		if (!isControl && controls != null) {
			children = controls.getChildNodes();
			count = children.getLength();

			for (int idx = 0; idx < count; idx++) {
				org.w3c.dom.Node n = children.item(idx);
				if (n instanceof Element) {
					Element el = (Element) n;
					writer.println("jspush(cx, ctlArr, ctl=jsnewobject(cx));");
					compileForm(writer, scriptName, "ctl",  el, true);
					if (hasError)
						return;
				}
			}

		}
	}


	public void parseForm(String scriptPath, String scriptName, String formName) {
		try {
			currentScript = scriptName;
			Document doc = builder.parse(new File(scriptPath));
			Element el = doc.getDocumentElement();
			Element rootDict = (Element) el.getElementsByTagName("dict").item(0);
			
			int globId = nextGlobalId++;
			defineGlobal(formName, globId);
			
			Integer formKind = (Integer) getPropertyDictionaryEntry(rootDict, "kind");
			
			formWriter.println("obj = slot[0] = jsnewobject(cx);");
			compileForm(formWriter, scriptName, "obj", rootDict, false);
			if (formKind == null || formKind == 0)
				formWriter.println("slot[1] = jsget(cx, cx->slots[GLOBAL_UI], " + hash("DefineWindow") + ", \"DefineWindow\");");
			else if (formKind == 1)
				formWriter.println("slot[1] = jsget(cx, cx->slots[GLOBAL_UI], " + hash("DefineSheet") + ", \"DefineSheet\");");
			else if (formKind == 2)
				formWriter.println("slot[1] = jsget(cx, cx->slots[GLOBAL_UI], " + hash("DefineControl") + ", \"DefineControl\");");
			else if (formKind == 3)
				formWriter.println("slot[1] = jsget(cx, cx->slots[GLOBAL_UI], " + hash("DefinePanel") + ", \"DefinePanel\");");
			else
				throw new RuntimeException("Unimplemented form kind");
			formWriter.println("callFrame[0] = slot[2];");
			formWriter.println(valueToString(getGlobalValue(formName)) + " = jsinvoke(cx, slot[1], cx->slots[GLOBAL_UI], &obj, 1);");
		} catch (Exception ex) {
			System.out.println(ex.toString());
			ex.printStackTrace();
			hasError = true;
			errorLine = -1;
			errorScript = scriptName;
			errorMessage = "An unexpected error occurred: " + ex.toString();	
		}
	}
	
	public void addImageRef(int globId, String name, String path) {
		constantLines.add("cx->slots[" + globId + "]=jsloadimage(cx," + encodeString(name) + ");");
	}

}
